當我們開發 Flutter 一段時間後,想必都會有自己習慣的開發方式跟技巧,但有時候很方便、速度快的方式卻不代表是好的,有可能開發上很省時卻導致性能有缺陷,記憶體使用過多。有實際去了解並驗證過嗎?當我們熟悉開發技巧、熟悉產品後,就會想要往高品質前進,希望提供的給用戶的東西是很棒的,這點沒錯吧!而良好的開發習慣也能幫助到自己或是團隊,不管是效率、程式碼可讀性、專案可維護性等等,這些是本文想要跟大家分享的內容,希望一起養成好習慣,我們馬上往下開始吧!
lazy computation
(惰性計算) 的特性,使用的時候才初始化,節省記憶體成本,並且只能對它賦值一次,在初始化後是無法改變的。我們可以在一開始給予數值或是使用方法的回傳值static const
和 static final
// 1.
late final String result = 'Hi';
// 2.
late final String result2 = _getComplexTaskResult();
String _getComplexTaskResult() {
return 'I am Yii.';
}
提醒:使用
late
的前提是必須知道你在做什麼,而不是盲目使用它,否則可能會發生不可預期的錯誤。因為已經跟 compiler 承諾,所以發生錯誤時是在 Runtime
盡可能地分離、縮小 Widget,建議 Widget 開發基於原子設計(Atomic Design),將頁面切分開來、將大區塊切割開來,每個元件都是基於其他元件而組成。
const
,有效避免 build 複雜的 Widget Treeconst
,可以在編譯期間就確認內容,不需要在 Runtime 時計算、檢查,也不能修改,提升整體效能和穩定性x = SizedBox.shrink();
y = SizedBox.shrink();
x == y // false
x = const SizedBox.shrink();
y = const SizedBox.shrink();
x == y // true
使用 Custom Widget 的好處有哪些以下幫你列出來:
const
constructor,並且當沒有動態參數要設置時,可以使用 const
。在每次的 rebuild 都可以省略此元件的處理,使用相同記憶體相同實體,不需要其他消耗context
,在進行一些 context 操作上會更適合,例如:存取 InheritedWidget,監聽狀態後的觸發刷新,可以精準處理而不會影響到其他元件,造成資源浪費。當然你可以使用 Builder 包裹來處理,但這不是最好的解法使用 functional-widget 沒辦法賦予 const
,每次 rebuild 都是一個消耗,記憶體使用上升
Widget Inspector 上查看到的會是第一個包裹元件,以例子來看就都是 Container,這裡不會顯示 function 名稱,在龐大的樹中你很難了解這是什麼元件、它在 APP 上的樣子
當錯發生時可以知道是哪個 function 出問題,不過資訊顯示上會比較多
假設有使用 Crashlytics 或是 Sentry 這類的錯誤捕捉服務,資訊會有所不同。以 Sentry 範例來看,標題為是顯示哪個 Route,也就是哪個頁面發生問題,沒有辦法精準定位。
當我們使用自定義的元件,在沒有動態參數的情境下,可以給予 const
,有效節省資源。並且可讀性、穩定性高
Widget Inspector 上的瀏覽很簡單、輕鬆,直接看出來是哪些元件,可讀性高,會更讓人願意使用工具幫忙解決問題
當發生錯誤時,在 Stack Trace 可以直接知道是哪個元件發生問題,資訊顯示上更精簡
Sentry 能搜集到的資訊也更明確,標題直接顯示哪個檔案的哪個元件有問題,下方的 Stack Trace 流程一樣很好理解
請養成創建元件的習慣,除了好處多以外,也幫專案品質跟團隊想想吧,讓自己和大家都能夠輕鬆開發。
const
constructorconst
,有設置 const
建構子,長寬不需設置也不會被約束影響,能以高效的方式實現空白 placeholderSizedBox.shrink()
來看,一開始就設置長寬為 0,不會佔 UI 任何空間如果專案裡沒有使用其他狀態管理框架,或是 Widget tree 龐大時,更新一個狀態就會導致整顆樹重建,這是個會降低性能的操作。這時候可以使用 StatefulBuilder 包裹提供元件,其中的 setState
可以用來更新指定元件,使用方式都一樣,讓其他不相關的元件可以保持原樣,不受影響。也很適合 Dialog 和 BottomSheet 相關元件使用,很方便的進行更新。
await showDialog<void>(
context: context,
builder: (BuildContext context) {
int? selectedRadio = 0;
return AlertDialog(
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: List<Widget>.generate(4, (int index) {
return Radio<int>(
value: index,
groupValue: selectedRadio,
onChanged: (int? value) {
setState(() => selectedRadio = value);
},
);
}),
);
},
),
);
},
);
build()
執行時是同步處理,但通常在跟使用者互動後(手勢操作、點擊按鈕…)有可能會觸發非同步任務,如果任務處理完後需要進行一些 context
的存取和操作,必須確保 Widget Tree 是否創建完成並且 element 沒有解除綁定(因為 context 本身就是 element),否則會出錯和崩潰of(context)
靜態函式的 InheritedWidget 存取預設的 flutter_lints
都會即時顯示提醒,說明不要在執行非同步任務後存取 BuildContexts。
需要在存取 BuildContexts 之前,先透過 mounted
確保 State 跟隨 Element 在樹,如果沒有則不進行後續處理。
ElevatedButton(
onPressed: () async {
await Future.delayed(const Duration(seconds: 2), () {});
if (!mounted) return;
Navigator.of(context).pop();
},
child: const Text('Pop page.'),
),
還有另一種方式,可先暫存需要的物件或資源,等非同步處理完後再透過物件進行操作。
ElevatedButton(
onPressed: () async {
ScaffoldMessengerState messengerState = ScaffoldMessenger.of(context);
await Future.delayed(const Duration(seconds: 2), () {});
messengerState.showSnackBar(const SnackBar(content: Text('Pop!')));
},
child: const Text('Pop page.'),
),
addListener()
監聽動畫更新後,在裡面使用 setState()
刷新元件。setState()
的目的是刷新整個 Widget Tree,但實際在大部分情境上,會受動畫數值影響的元件只是其中一小部分,這個錯誤的使用方式將導致重建整個 UI,影響到其他元件,可能會讓畫面延遲、卡頓,造成體驗不佳void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_animationController.addListener(() => setState(() {}));
_animationController.forward();
}
void initState() {
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
// No addListener() and setState()
_animationController.forward();
}
在使用 AnimatedBuilder 時,記得將不需要動畫、不會受動畫影響的子元件透過 child
參數設置,並在 builder
裡拿來使用。
markNeedsPaint()
直接刷新AnimatedBuilder(
animation: _animationController,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
),
builder: (context, child) {
return Opacity(
opacity: _animationController.value,
child: child,
);
},
),
FadeTransition(
opacity: _animationController,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
),
),
AnimatedOpacity(
opacity: isVisible ? 0 : 1,
duration: const Duration(seconds: 1),
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
),
),
builder()
代表只創建即將顯示和在畫面上的元件,這些 item 屬於 lazy loaded。一般的建構子方式,會導致如果有 1000 個元件,全部都會在一開始就創建,體驗上很差。
SliverList.builder()
SliverGrid.builder()
ListView.builder()
GridView.builder()
InteractiveViewer.builder()
TableView.builder()
。表格瀏覽,跟隨 Flutter 3.13 推出,可安裝 two_dimensional_scrollables 套件使用prototypeItem
屬性設置,提升效能ListView.builder(
itemCount: 500,
itemExtent: 100,
itemBuilder: (context, index) {
return Container();
},
),
scrollDirection
指定方向的最大範圍,先確認滑動空間。所以我們都會使用 Expanded 來包裹 ScrollView,否則會報錯shrinkWrap
為 true,但是這個情境下 ScrollView 就會根據內容的變動、多寡來頻繁計算需要顯示的滾動空間,以達成收縮效果,它的代價就是成本很高,一樣會影響 APP 性能ListView.builder(
itemCount: 500,
itemExtent: 100,
shrinkWrap: true,
itemBuilder: (context, index) {
return Container();
},
),
預設情況下 item-widget 保持活動狀態,不會再重新繪製,也不會在可視範圍之外被垃圾回收。實際使用者操作滾動時,原本的 item 雖然沒有在畫面上顯示,但是一樣存在,滾動回來後直接顯示,不需要繪製消耗資源,為了確保滑動順暢
// 預設為true,讓每個item保持活動,不被銷毀
addAutomaticKeepAlives: true
// 預設為true,每個item都用RepainBoundry包裝,它只繪製一次以獲得更高的性能
addRepaintBoundaries: true
false
,可能會導致使用更多 CPU 和 GPU 工作,因為需要重新繪製並管理狀態,但它可以解決記憶體問題,並且同時獲得所需情境下的效果。不過還是要根據實際狀況來評估,請嘗試後再做決定,透過 DevTools 協助我們
ListView.builder(
itemCount: 500,
itemExtent: 100,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemBuilder: (context, index) {
return Image.asset('assets/images/big_image.png');
},
)
本文說明了一些提高 APP 性能的開發觀念與技巧,讓我們可以在節省資源的情況下發揮最好表現,讓產品順暢運行且保持穩定。很多問題都是由細小的原因累積而成,不要覺得隨意開發專案還是保持順暢,可能只是我們覺得,實際上在用戶的裝置上並不理想,所以開發時的每個細節都很重要。同時記得要透過 DevTools 協助開發,養成好習慣,以後會感謝自己的。